package main

import (
	"bytes"
	"fmt"
	"go/format"
	"io"
	"os"
	"sort"
	"strings"

	flag "github.com/spf13/pflag"

	"sigs.k8s.io/controller-tools/pkg/genall"
	"sigs.k8s.io/controller-tools/pkg/genall/help"
	prettyhelp "sigs.k8s.io/controller-tools/pkg/genall/help/pretty"
	"sigs.k8s.io/controller-tools/pkg/markers"
)

var (
	optionsRegistry = &markers.Registry{}
	genMarker       = markers.Must(markers.MakeDefinition("generate", markers.DescribesPackage, Generator{}))
)

func init() {
	if err := optionsRegistry.Register(genMarker); err != nil {
		panic(err)
	}
	if help := (Generator{}).Help(); help != nil {
		optionsRegistry.AddHelp(genMarker, help)
	}
	if err := genall.RegisterOptionsMarkers(optionsRegistry); err != nil {
		panic(err)
	}
}

const (
	markerName = "controllertools:marker:generateHelp"
	header     = `
// +build !ignore_autogenerated

%[2]s

// Code generated by helpgen. DO NOT EDIT.

package %[1]s

import (
	"sigs.k8s.io/controller-tools/pkg/markers"
)

`
	funcBlock = `
func (%[1]s) Help() *markers.DefinitionHelp {
	return &markers.DefinitionHelp{
		Category: %[2]q,
		DetailedHelp: markers.DetailedHelp{
			Summary: %[3]q,
			Details: %[4]q,
		},
		FieldHelp: map[string]markers.DetailedHelp{
			%[5]s
		},
	}
}
`
)

type generateHelp struct {
	Category string `marker:",optional"`
}

func godocToDetails(typeName string, doc string) (summary, details string) {
	docParts := strings.SplitN(doc, "\n", 2)
	summary = docParts[0]
	if summaryWords := strings.SplitN(summary, " ", 2); summaryWords[0] == typeName {
		if len(summaryWords) > 1 {
			summary = summaryWords[1]
		} else {
			summary = ""
		}
	}

	details = ""
	if len(docParts) > 1 {
		details = strings.TrimSpace(docParts[1])
	}
	return summary, details
}

type Generator struct {
	HeaderFile string `marker:",optional"`
	Year       string `marker:",optional"`
}

func (Generator) RegisterMarkers(reg *markers.Registry) error {
	defn := markers.Must(markers.MakeDefinition(markerName, markers.DescribesType, generateHelp{}))
	if err := reg.Register(defn); err != nil {
		return err
	}
	reg.AddHelp(defn, &markers.DefinitionHelp{
		Category: "markers",
		DetailedHelp: markers.DetailedHelp{
			Summary: "generate help based on the godoc of this marker type.",
			Details: "Type-level godoc becomes general marker summary (first line) and details (other lines).  Field-level godoc similarly becomes marker field help.",
		},
		FieldHelp: map[string]markers.DetailedHelp{
			"Category": markers.DetailedHelp{
				Summary: "indicates the general category to which this marker belongs",
			},
		},
	})
	return nil
}
func (g Generator) Generate(ctx *genall.GenerationContext) error {
	var headerText string
	if g.HeaderFile != "" {
		headerBytes, err := ctx.ReadFile(g.HeaderFile)
		if err != nil {
			return err
		}
		headerText = string(headerBytes)
	}
	headerText = strings.ReplaceAll(headerText, " YEAR", g.Year)

	for _, root := range ctx.Roots {
		byType := make(map[string]string)
		if err := markers.EachType(ctx.Collector, root, func(info *markers.TypeInfo) {
			markerVal, hadMarker := info.Markers.Get(markerName).(generateHelp)
			if !hadMarker {
				return
			}
			outContent := new(bytes.Buffer)

			for _, field := range info.Fields {
				summary, details := godocToDetails(field.Name, field.Doc)
				fmt.Fprintf(outContent, "%[1]q: markers.DetailedHelp{\nSummary: %[2]q,\n Details: %[3]q,\n},\n", field.Name, summary, details)
			}

			summary, details := godocToDetails(info.Name, info.Doc)
			byType[info.Name] = fmt.Sprintf(funcBlock, info.Name, markerVal.Category, summary, details, outContent.String())
		}); err != nil {
			return err
		}

		if len(byType) == 0 {
			continue
		}

		// ensure a stable output order
		typeNames := make([]string, 0, len(byType))
		for typ := range byType {
			typeNames = append(typeNames, typ)
		}
		sort.Strings(typeNames)

		outContent := new(bytes.Buffer)
		if headerText == "" {
			panic("at the disco!")
		}
		fmt.Fprintf(outContent, header, root.Name, headerText)

		for _, typ := range typeNames {
			fmt.Fprintln(outContent, byType[typ])
		}

		outBytes := outContent.Bytes()
		if formatted, err := format.Source(outBytes); err != nil {
			root.AddError(err)
		} else {
			outBytes = formatted
		}

		outputFile, err := ctx.Open(root, "zz_generated.markerhelp.go")
		if err != nil {
			root.AddError(err)
			continue
		}
		defer outputFile.Close()
		n, err := outputFile.Write(outBytes)
		if err != nil {
			root.AddError(err)
			continue
		}
		if n < len(outBytes) {
			root.AddError(io.ErrShortWrite)
		}
	}
	return nil
}

func (Generator) Help() *markers.DefinitionHelp {
	// need to write this by hand, otherwise we'd have a bootstrap issue
	return &markers.DefinitionHelp{
		Category: "",
		DetailedHelp: markers.DetailedHelp{
			Summary: "generates marker help using godoc, based on the presence of a particular marker",
		},
		FieldHelp: map[string]markers.DetailedHelp{
			"HeaderFile": markers.DetailedHelp{Summary: "the file containing the header to use for generated files"},
			"Year":       markers.DetailedHelp{Summary: "replace \" YEAR\" in the header with this value."},
		},
	}
}

func usage() {
	fmt.Fprintf(os.Stderr, "usage: %s generate:headerFile=./boilerplate.go.txt,year=2019 paths=./pkg/...\n", os.Args[0])

	optInfo := help.ByCategory(optionsRegistry, help.SortByOption)
	for _, cat := range optInfo {
		if cat.Category == "" {
			continue
		}
		contents := prettyhelp.MarkersDetails(true, cat.Category, cat.Markers)
		if err := contents.WriteTo(os.Stderr); err != nil {
			panic(err)
		}
	}

	reg := &markers.Registry{}
	if err := (Generator{}).RegisterMarkers(reg); err != nil {
		panic(err)
	}
	markerInfo := help.ByCategory(reg, help.SortByCategory)
	contents := prettyhelp.MarkersDetails(true, "markers", markerInfo[0].Markers)
	if err := contents.WriteTo(os.Stderr); err != nil {
		panic(err)
	}
	os.Exit(1)
}

func main() {
	help := flag.BoolP("help", "h", false, "print help")
	flag.Parse()

	if *help {
		usage()
	}

	if len(os.Args) > 1 && os.Args[1] == "--" {
		os.Args = append([]string{os.Args[0]}, os.Args[2:]...)
	}

	runtime, err := genall.FromOptions(optionsRegistry, os.Args[1:])
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error: %v\n", err)
		usage()
	}
	runtime.OutputRules.Default = genall.OutputArtifacts{}

	if hadErrs := runtime.Run(); hadErrs {
		fmt.Fprintln(os.Stderr, "not entirely successful")
		os.Exit(1)
	}
}
